package com.qianfeng.smartdevice.websocket;

import com.qianfeng.smartdevice.event.CheckDeviceStatusTaskEvent;
import com.qianfeng.smartdevice.event.DeviceStatusChangeEvent;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Component;
import org.springframework.web.socket.CloseStatus;
import org.springframework.web.socket.TextMessage;
import org.springframework.web.socket.WebSocketSession;
import org.springframework.web.socket.config.annotation.EnableWebSocket;
import org.springframework.web.socket.handler.TextWebSocketHandler;

import java.io.IOException;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

@Component
@EnableWebSocket
public class MyBaseWebsocketHandler extends TextWebSocketHandler {
    //我们的目的是通过当前 websocket 给指定的设备推送命令
    //哪个设备?是一个设备吗? 很多个设备
    //每个设备的会话是什么?
    //保存的是所有的设备的会话
    protected Map<String, WebSocketSession> allClients = new HashMap<>();

    protected Map<String, Long> lastConnectionTime = new HashMap<>();//记录设备最后一次连接时间的

    protected ApplicationContext context;

    @Autowired
    public void setContext(ApplicationContext context) {
        this.context = context;
    }

    public void afterConnectionEstablished(WebSocketSession session) throws Exception {
        //当设备和服务器建立连接的时候执行,保存会话
        //我们约定地址的最后一位是设备的编码
        String uri = session.getUri().toString();
        String uuid = uri.substring(uri.lastIndexOf("/") + 1);//获取设备的唯一标识
        session.getAttributes().put("uuid", uuid);//为了方便后面随时获取 uuid,我们保存了一份
        allClients.put(uuid, session);
        System.err.println("收到 websocket 请求了" + uuid);
        lastConnectionTime.put(uuid, System.currentTimeMillis());//保存设备的连接时间
        //当前方法执行的时候代表设备连接到服务器了,需要更新状态,需要修改当前设备在数据库中的状态为现在
        //当前设备是谁?我们要求设备以 uuid 作为标识来连接我们的 websocket,所以上面的 uuid 就是设备的 uuid
        //我们可以以 uuid作为条件,更新设备的在线状态
        //更新状态和 websocket 建立连接这两个操作有直接关系吗? 更新数据库失败要不要将设备下线?不需要,也就是说我们只是通过这个方法来知道设备上线而已,更新状态是独立的操作
        context.publishEvent(DeviceStatusChangeEvent.onLine(uuid));
    }


//     如果设备是因为断电断网等非正常原因导致的断开连接,服务器不会受到断开的操作,所以该方法在非正常的情况下不会执行
//     我们可以通过记录设备的最后一次心跳时间的方式来判断,比如超过一分钟没有发送过心跳数据的我们就认为你掉线了
//     所以需要记录设备的最后一次心跳时间,每次心跳都需要更新时间的,什么时候有心跳,我们可以简单的认为心跳就是设备向服务器发送了一个数据
//     但是设备什么时候掉线也不确定,在任何时间点都可能会掉线,所以我们只能循环检查这个事情,定时任务
    public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {
        String uuid = (String) session.getAttributes().get("uuid");
        allClients.remove(uuid);//当会话关闭的时候从所有的客户端会话保存的 map 中删除
        System.err.println("设备断开连接了" + uuid);
        context.publishEvent(DeviceStatusChangeEvent.offLine(uuid));//更新设备状态为离线
    }


    //处理文本消息的
    protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {

        String uuid = (String) session.getAttributes().get("uuid");//获取到发送心跳数据的设备的 uuid
        //现在设备没发送一次数据我们就更新一次时间,所以必须有个地方保存了更新时间
        System.err.println("收到设备 的心跳数据了");
        lastConnectionTime.put(uuid, System.currentTimeMillis());

    }

    @EventListener
    public void onEvent(CheckDeviceStatusTaskEvent event) {
        if (!isNeedCheckStatus()) {//不需要检查心跳
            return;
        }
        System.err.println("websocket收到了检查状态的事件,准备开始检查数据");
        //遍历在线时间的 map 的 uuid,找到所有超出时间的 key,先删掉这个数据,然后将 uuid 放到一个新的集合中返回
        List<String> list = lastConnectionTime.keySet().stream().filter(uuid -> {

            //获取当前的时间
            long currentTimeMillis = System.currentTimeMillis();
            Long time = lastConnectionTime.get(uuid);//获取当前设备的上次心跳时间

            if (currentTimeMillis - time >= 60000) {
                //当前时间和上次的心跳时间已经超出了一分钟
                WebSocketSession session = allClients.get(uuid);
                allClients.remove(uuid);//因为设备已经离线了,所以从保存有所有会话的 map 中删除数据
                //不能光只从 map 中删除,还需要关闭连接
                if (session != null && session.isOpen()) {
                    try {
                        session.close();//关闭连接,会触发关闭的监听,在内部我们已经做了更新数据的事件
                        session = null;
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
            }
            return currentTimeMillis - time >= 60000; //只要离线的设备
        }).collect(Collectors.toList());

        //遍历所有离线的设备的 uuid 集合,然后从lastConnectionTime删除掉指定的数据
        list.forEach(uuid->{
            lastConnectionTime.remove(uuid);
        });
    }




    //如果不需要检查设备的状态,返回 false
    protected boolean isNeedCheckStatus() {
        return true;
    }
}
